package asteroids.engine;

import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.geom.Line2D;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.apache.log4j.Logger;

import asteroids.entity.GameObject;

public class Utils
{
    static Logger logger = Logger.getLogger(Utils.class);

    static final double DEGREES_PER_RADIAN = 57.2957795;
    static final double RADIANS_PER_DEGREE = 0.0174532925;
    static final private Utils utils = new Utils();
    public static final int SCREEN_WIDTH = Toolkit.getDefaultToolkit().getScreenSize().width;
    public static final int SCREEN_HEIGHT = Toolkit.getDefaultToolkit().getScreenSize().height;
    public static final int GAME_WIDTH = 640;
    public static final int GAME_HEIGHT = 480;
    // public static final int GAME_WIDTH = SCREEN_WIDTH;
    // public static final int GAME_HEIGHT = SCREEN_HEIGHT;

    public static final int FRAMES_PER_SECOND = 24;
    public static final int MILLISECONDS_PER_SECOND = 1000;
    public static final int MILLISECONDS_PER_FRAME = MILLISECONDS_PER_SECOND / FRAMES_PER_SECOND;

    // coefficient of kinetic friction in space... whoah
    public static final double MU = 0.02;

    /** Defines a single point. **/
    public static final PolarCoordinate[] POINT_SHAPE = { Utils.createPolarCoordinate(0, 0),
            Utils.createPolarCoordinate(0, 0) };

    private Utils()
    {
    }

    static public double radiansFromDegrees(double degrees)
    {
        return degrees * RADIANS_PER_DEGREE;
    }

    static public double degreesFromRadians(double rads)
    {
        return rads * DEGREES_PER_RADIAN;
    }

    /**
     * Takes in a PolarCoordinate, returns a CartesianCoordinate equivalent
     * 
     * @param p
     * @return
     */
    static public CartesianCoordinate cartesianFromPolar(PolarCoordinate p)
    {
        return utils.new CartesianCoordinate(p.getRadius()
                * Math.cos(radiansFromDegrees(p.getRotation())), p.getRadius()
                * Math.sin(radiansFromDegrees(p.getRotation())));
    }

    /**
     * constructs a PolarCoordinate from the given CartesianCoordinate, assuming
     * that the x- and y-coordinates are given relative to zero
     * 
     * @param x
     * @param y
     * @return
     */
    static public PolarCoordinate polarFromCartesian(CartesianCoordinate cc)
    {
        double xvec = cc.getXComponent();
        double yvec = cc.getYComponent();
        double rotation = 0;

        if( xvec == 0 && yvec == 0 )
        {
            rotation = 0; // because, whatever
        }
        else if( xvec >= 0 && yvec > 0 ) // going down and right - quadrant 1
        {
            logger.debug("quad 1");
            rotation = Math.abs(Utils.degreesFromRadians(Math.atan(yvec / xvec)));
            rotation = (Double.isNaN(rotation)) ? 0 : rotation;
        }
        else if( xvec < 0 && yvec >= 0 ) // going right and up - quad 2
        {
            rotation = Math.abs(Utils.degreesFromRadians(Math.atan(xvec / yvec)));
            rotation = (Double.isNaN(rotation)) ? 0 : rotation;
            rotation += 90;
            logger.debug("quad 2");
            logger.debug("direction = " + rotation);
        }
        else if( xvec < 0 && yvec < 0 ) // quad 3
        {
            rotation = Math.abs(Utils.degreesFromRadians(Math.atan(yvec / xvec)));
            rotation = (Double.isNaN(rotation)) ? 0 : rotation;
            rotation += 180;
            logger.debug("quad 3");
            logger.debug("direction = " + rotation);
        }
        else if( xvec >= 0 && yvec <= 0 ) // quad 4
        {
            rotation = Math.abs(Utils.degreesFromRadians(Math.atan(xvec / yvec)));
            rotation = (Double.isNaN(rotation)) ? 0 : rotation;
            rotation += 270;
            logger.debug("quad 4");
            logger.debug("direction = " + rotation);
        }

        return utils.new PolarCoordinate(rotation, Math.sqrt(xvec * xvec + yvec * yvec));
    }

    /**
     * returns the direction in degrees from point x1, y1 to point x2, y2
     * 
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @return
     */
    public static double pointDirection(double x1, double y1, double x2, double y2)
    {
        double xcomp = x2 - x1;
        double ycomp = y2 - y1;

        return polarFromCartesian(createCartesianCoordinate(xcomp, ycomp)).getRotation();
    }

    /**
     * returns a Utils.PolarCoordinate with the supplied angle and radius
     * 
     * @param theta
     * @param radius
     * @return
     */
    public static PolarCoordinate createPolarCoordinate(double theta, double radius)
    {
        return utils.new PolarCoordinate(theta, radius);
    }

    public static PolarCoordinate[] createPolarCircle(double radius)
    {
        Utils.PolarCoordinate[] circle = { Utils.createPolarCoordinate(0, radius),
                Utils.createPolarCoordinate(45, radius), Utils.createPolarCoordinate(90, radius),
                Utils.createPolarCoordinate(135, radius), Utils.createPolarCoordinate(180, radius),
                Utils.createPolarCoordinate(225, radius), Utils.createPolarCoordinate(270, radius),
                Utils.createPolarCoordinate(315, radius) };

        return circle;
    }

    public static Utils.PolarCoordinate[] scalePolarShape(Utils.PolarCoordinate[] shape,
            double factor)
    {
        Utils.PolarCoordinate[] newShape = new Utils.PolarCoordinate[shape.length];
        for( int i = 0; i < shape.length; i++ )
        {
            newShape[i] = shape[i].scaleBy(factor);
        }
        return newShape;
    }

    public static void drawCartesianPolygon(CartesianCoordinate[] coords, Graphics g)
    {
        for( int i = 0; i < coords.length; i++ )
        {
            g.drawLine((int) Math.round(coords[i].getXComponent()),
                    (int) Math.round(coords[i].getYComponent()),
                    (int) Math.round(coords[(i + 1) % coords.length].getXComponent()),
                    (int) Math.round(coords[(i + 1) % coords.length].getYComponent()));
        }
    }

    public static CartesianCoordinate createCartesianCoordinate(double xpos, double ypos)
    {
        return utils.new CartesianCoordinate(xpos, ypos);
    }

    /**
     * check that the objects are close enough to actually collide if so,
     * extract polygons and do real collision check.
     * 
     * @param a
     * @param b
     * @return
     */
    public static boolean gameObjectsCollide(GameObject a, GameObject b)
    {
        logger.trace("checking collision between " + a + " and " + b);
        // grab some shorter variable names
        double ax = a.getXPosition();
        double ay = a.getYPosition();
        double bx = b.getXPosition();
        double by = b.getYPosition();
        Utils.CartesianCoordinate[] abounds = a.getCollisionBounds();
        Utils.CartesianCoordinate[] bbounds = b.getCollisionBounds();
        // if either is overlapping the edge, shift both hitboxes by an
        // amount
        // that places a in the middle if the field
        if( overlapsGameEdge(a) || overlapsGameEdge(b) )
        {
            double shiftx = (Utils.GAME_WIDTH / 2.0) - ax;
            double shifty = (Utils.GAME_HEIGHT / 2.0) - ay;

            // shift a
            ax += shiftx;
            ay += shifty;

            // shift a's collision bounds. a is now logically centered in the
            // screen.
            for( int i = 0; i < abounds.length; i++ )
            {
                abounds[i] = abounds[i].shiftBy(shiftx, shifty);
            }

            // if the transformation places b outside the game field, wrap him
            // back around.
            if( bx + shiftx > Utils.GAME_WIDTH )
            {
                shiftx -= Utils.GAME_WIDTH;
            }
            if( by + shifty > Utils.GAME_HEIGHT )
            {
                shifty -= Utils.GAME_HEIGHT;
            }
            if( bx + shiftx < 0 )
            {
                shiftx += Utils.GAME_WIDTH;
            }
            if( by + shifty < 0 )
            {
                shifty += Utils.GAME_HEIGHT;
            }

            // now we actually shift b's center
            bx += shiftx;
            by += shifty;

            // finally, recalculate b's collision bounds as well.
            for( int i = 0; i < bbounds.length; i++ )
            {
                bbounds[i] = bbounds[i].shiftBy(shiftx, shifty);
            }
        }

        // if a and b are still too far away to collide, then they really don't
        // collide.
        if( pointDistance(ax, ay, bx, by) > a.getMaxRadius() + b.getMaxRadius() )
        {
            return false;
        }
        else
        {
            return collides(abounds, bbounds);
        }
    }

    public static boolean overlapsGameEdge(GameObject g)
    {
        if( g.getXPosition() > Utils.GAME_WIDTH - g.getMaxRadius()
                || g.getXPosition() < g.getMaxRadius()
                || g.getYPosition() > Utils.GAME_HEIGHT - g.getMaxRadius()
                || g.getYPosition() < g.getMaxRadius() )
        {
            return true;
        }

        return false;
    }

    public static double pointDistance(CartesianCoordinate c1, CartesianCoordinate c2)
    {
        return pointDistance(c1.getXComponent(), c1.getYComponent(), c2.getXComponent(),
                c2.getYComponent());
    }

    /**
     * uses Pythagoras to get the distance between two points
     */
    public static double pointDistance(double p1x, double p1y, double p2x, double p2y)
    {
        return Math.sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y));
    }

    /**
     * compares two polygons to see if they are in a collision state
     * 
     * @param cartShape1
     * @param cartShape2
     * @return
     */
    public static boolean collides(CartesianCoordinate[] cartShape1,
            CartesianCoordinate[] cartShape2)
    {
        logger.trace("Entering Utils.collides()");

        if( pointsCollide(cartShape1, cartShape2) )
        {
            logger.trace("Points DO collide");
            return true;
        }

        logger.trace("exiting Utils.collides()");
        return shapeCollides(cartShape1, cartShape2);
    }

    private static boolean pointsCollide(CartesianCoordinate[] cartShape1,
            CartesianCoordinate[] cartShape2)
    {
        logger.trace("entering pointsCollide()");
        // check if any point from shape1 lies inside shape 2
        for( CartesianCoordinate point : cartShape1 )
        {
            logger.debug("checking point (" + point.getXComponent() + ", " + point.getYComponent()
                    + ") from shape1");
            if( pointIsInPolygon(point, cartShape2) )
            {
                logger.trace("returning true from pointsCollide()");
                return true;
            }
        }
        // if not, check points from shape2 against shape1
        for( CartesianCoordinate point : cartShape2 )
        {
            logger.debug("checking point (" + point.getXComponent() + ", " + point.getYComponent()
                    + ") from shape2");
            if( pointIsInPolygon(point, cartShape1) )
            {
                logger.trace("returning true from pointsCollide()");
                return true;
            }
        }

        logger.trace("returning false from pointsCollide()");
        return false;
    }

    public static boolean pointIsInGameArea(double x, double y)
    {
        CartesianCoordinate point = createCartesianCoordinate(x, y);
        CartesianCoordinate[] gameArea = { createCartesianCoordinate(0, 0),
                createCartesianCoordinate(GAME_WIDTH, 0),
                createCartesianCoordinate(GAME_WIDTH, GAME_HEIGHT),
                createCartesianCoordinate(0, GAME_HEIGHT) };
        return pointIsInPolygon(point, gameArea);
    }

    /**
     * counts the number of the shape's lines that cross above the point. If
     * odd, the point is inside the shape.
     * 
     * @param point
     * @param shape
     * @return
     */
    private static boolean pointIsInPolygon(CartesianCoordinate point, CartesianCoordinate[] shape)
    {
        logger.trace("entering pointIsInPolygon()");
        int intersectCount = 0;
        CartesianCoordinate lastPoint = shape[shape.length - 1];

        logger.trace(" point: (" + point.getXComponent() + ", " + point.getYComponent() + ")");

        for( CartesianCoordinate thisPoint : shape )
        {
            logger.debug(" line: " + lastPoint + " to " + thisPoint);
            // compare each line segment from shape2 against a tall vertical
            // line passing through 'point'
            // only count intersections that occur above the point in question
            // CartesianCoordinate intersection = getIntersection(
            // utils.new CartesianCoordinate(point.getXPosition(),
            // point.getYPosition()),
            // utils.new CartesianCoordinate(point.getXPosition(), GAME_HEIGHT),
            // lastPoint, thisPoint);
            if( Line2D.linesIntersect(point.getXComponent(), point.getYComponent(),
                    point.getXComponent(), 0, lastPoint.getXComponent(), lastPoint.getYComponent(),
                    thisPoint.getXComponent(), thisPoint.getYComponent()) )
            {
                intersectCount = intersectCount + 1;
                logger.debug(" intersectCount = " + intersectCount);
            }

            // logger.debug("intersection? " + intersection);

            // if(intersection != null && intersection.getYPosition() >=
            // point.getYPosition())
            // {
            // intersectCount = intersectCount + 1;
            // logger.debug("intersectCount = " + intersectCount);
            // }
            lastPoint = thisPoint; // go to next line segment in the shape.
        }

        logger.trace("returning " + (intersectCount % 2 != 0) + " from pointIsInPolygon()");
        return (intersectCount % 2 != 0);
    }

    /**
     * Checks if any line from cartShape1 intersects any line from cartShape2
     * 
     * @param cartShape1
     * @param cartShape2
     * @return
     */
    private static boolean shapeCollides(CartesianCoordinate[] cartShape1,
            CartesianCoordinate[] cartShape2)
    {
        logger.trace("Entering shapeCollides()");
        boolean collides = false;

        CartesianCoordinate lastPoint1 = cartShape1[cartShape1.length - 1];
        CartesianCoordinate lastPoint2 = cartShape2[cartShape2.length - 1];

        // check if any line from cartShape1 intersects any line from cartShape2
        for( CartesianCoordinate thisPoint1 : cartShape1 )
        {
            for( CartesianCoordinate thisPoint2 : cartShape2 )
            {
                if( Line2D.linesIntersect(lastPoint1.getXComponent(), lastPoint1.getYComponent(),
                        thisPoint1.getXComponent(), thisPoint1.getYComponent(),
                        lastPoint2.getXComponent(), lastPoint2.getYComponent(),
                        thisPoint2.getXComponent(), thisPoint2.getYComponent()) )// !=
                                                                                 // null)
                {
                    collides = true;
                    break;
                }
                lastPoint2 = thisPoint2;
            }
            lastPoint1 = thisPoint1;
        }

        logger.trace("returning " + collides + " from collides");
        return collides;
    }

    // /**
    // * @returns a CartesianCoordinate representing the intersection between
    // the line from line1Point1 to line1Point2 and the line from
    // * line2Point1 to line1Point2
    // */
    // private static CartesianCoordinate getIntersection(CartesianCoordinate
    // line1P1, CartesianCoordinate line1P2, CartesianCoordinate line2P1,
    // CartesianCoordinate line2P2)
    // {
    // logger.trace("Entering getIntersection()");
    // CartesianCoordinate intersection = null;
    // CartesianCoordinate line1Point1 = line1P1.shiftBy(0, 0);
    // CartesianCoordinate line1Point2 = line1P2.shiftBy(0, 0);
    // CartesianCoordinate line2Point1 = line2P1.shiftBy(0, 0);
    // CartesianCoordinate line2Point2 = line2P2.shiftBy(0, 0);
    //
    // // if either line is near vertical, skew it slightly
    // if(line1Point1.getXPosition() - line1Point2.getXPosition() < EPSILON){
    // line1Point1 = line1Point1.shiftBy(EPSILON, 0);
    // line1Point2 = line1Point2.shiftBy(-EPSILON, 0);
    // }
    // if(line2Point1.getXPosition() - line2Point2.getXPosition() < EPSILON){
    // line2Point1 = line2Point1.shiftBy(EPSILON, 0);
    // line2Point2 = line2Point2.shiftBy(-EPSILON, 0);
    // }
    //
    // double A1 = line1Point2.getYPosition() - line1Point1.getYPosition();
    // double B1 = line1Point1.getXPosition() - line1Point2.getXPosition();
    // double C1 = A1*line1Point1.getXPosition() +
    // B1*line1Point1.getYPosition();
    // logger.debug("line1 is from (" + line1Point1.getXPosition() + ", " +
    // line1Point1.getYPosition() + ")");
    // logger.debug("\tto (" + line1Point2.getXPosition() + ", " +
    // line1Point2.getYPosition() + ")");
    //
    // double A2 = line2Point2.getYPosition() - line2Point1.getYPosition();
    // double B2 = line2Point1.getXPosition() - line2Point2.getXPosition();
    // double C2 = A2*line2Point1.getXPosition() +
    // B2*line2Point1.getYPosition();
    // logger.debug("line2 is from (" + line2Point1.getXPosition() + ", " +
    // line2Point1.getYPosition() + ")");
    // logger.debug("\tto (" + line2Point2.getXPosition() + ", " +
    // line2Point2.getYPosition() + ")");
    //
    // double det = A1*B2 + A2*B1;
    // logger.debug("determinant: " + det);
    //
    // if(det != 0) // the two lines do intersect... but do the line segments?
    // {
    // double x_intersect = (B2*C1 - B1*C2) / det;
    // double y_intersect = (A1*C2 - A2*C1) / det;
    // logger.debug("intersect is (" + x_intersect + ", " + y_intersect + ")");
    // if ( (line1Point1.getXPosition() <= x_intersect) == (x_intersect <=
    // line1Point2.getXPosition()) &&
    // (line1Point1.getYPosition() <= y_intersect) == (y_intersect <=
    // line1Point2.getYPosition()) &&
    // (line2Point1.getXPosition() <= x_intersect) == (x_intersect <=
    // line2Point2.getXPosition()) &&
    // (line2Point1.getYPosition() <= y_intersect) == (y_intersect <=
    // line2Point2.getYPosition()))
    // {
    // // then the point (x_intersect, y_intersect) lies in both line segments.
    // intersection = utils.new CartesianCoordinate(x_intersect, y_intersect);
    // }
    // }
    //
    // logger.trace("Exiting getIntersection()");
    // return intersection;
    // }

    public static CartesianCoordinate[] cartesianShapeFromPolarShape(PolarCoordinate[] polarShape)
    {
        CartesianCoordinate[] cartShape = new CartesianCoordinate[polarShape.length];

        for( int i = 0; i < polarShape.length; i++ )
        {
            // convert the polar coordinates to Cartesian ... and shift to
            // account for the position of the ship
            cartShape[i] = Utils.cartesianFromPolar(polarShape[i]);
        }

        return cartShape;
    }

    public class PolarCoordinate
    {
        final double theta;
        final double radius;

        public PolarCoordinate(double t, double r)
        {
            theta = t;
            radius = r;
        }

        // public CartesianCoordinate toCartesian()
        // {
        // return new CartesianCoordinate(
        // radius*Math.cos(radiansFromDegrees(theta)),
        // radius*Math.sin(radiansFromDegrees(theta)));
        // }

        public double getRadius()
        {
            return radius;
        }

        /**
         * @return the angle of the polar coordinates in degrees
         */
        public double getRotation()
        {
            return theta;
        }

        public PolarCoordinate rotateBy(double degrees)
        {
            return utils.new PolarCoordinate((theta + degrees) % 360, radius);
        }

        public PolarCoordinate scaleBy(double factor)
        {
            return utils.new PolarCoordinate(theta, radius * factor);
        }

        public String toString()
        {
            return "(" + theta + ", " + radius + ")";
        }
    }

    public class CartesianCoordinate
    {
        final double x_position;
        final double y_position;

        public CartesianCoordinate(double x, double y)
        {
            x_position = x;
            y_position = y;
        }

        public double getXComponent()
        {
            return x_position;
        }

        public double getYComponent()
        {
            return y_position;
        }

        public CartesianCoordinate shiftBy(double x, double y)
        {
            return utils.new CartesianCoordinate(x_position + x, y_position + y);
        }

        public CartesianCoordinate scaleBy(double factor)
        {
            return utils.new CartesianCoordinate(x_position * factor, y_position * factor);
        }

        public String toString()
        {
            return "(" + x_position + ", " + y_position + ")";
        }
    }

    /**
     * sorts a collection of GameObjects according to their distance from the
     * given point
     * 
     * @param list
     * @param point
     */
    public static void sortByDistance(List<? extends GameObject> list, CartesianCoordinate point)
    {
        Collections.sort(list, utils.new distanceComparator(point));
    }

    /**
     * Comparator used to sort an array by distance from a chosen point
     * 
     * @author whitecd1
     * 
     */
    private class distanceComparator implements Comparator<GameObject>
    {
        Utils.CartesianCoordinate reticule;

        public distanceComparator(Utils.CartesianCoordinate reticule)
        {
            this.reticule = reticule;
        }

        @Override
        public int compare(GameObject s0, GameObject s1)
        {
            int dist0 = (int) Utils.pointDistance(s0.getXPosition(), s0.getYPosition(),
                    reticule.getXComponent(), reticule.getYComponent());
            int dist1 = (int) Utils.pointDistance(s1.getXPosition(), s1.getYPosition(),
                    reticule.getXComponent(), reticule.getYComponent());

            return dist0 - dist1;
        }
    }
}
